# -*- coding: utf-8 -*-

import os
import logging
import sys
import traceback
from functools import partial
from datetime import datetime

from flask import current_app, jsonify, request, render_template, \
    redirect, url_for, session
from flask.views import MethodView
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
import humanfriendly as hf

from mpdev import db
from mpdev.gzone_loader import login_admin, logout_admin, admin_required,\
    current_user, current_room
from . import gzone
from . import models
from . import forms

logger = logging.getLogger(__name__)

DEFAULT_LOGIN_SUCCESS_REDIRECT_URL = '/gzone'
DEFAULT_LOGOUT_SUCCESS_REDIRECT_URL = '/gzone'
DT_TO_STR_FORMAT = '%Y-%m-%dT%H:%M:%M'


def get_setting(name, blueprint_name='gzone'):
    try:
        return current_app.config[blueprint_name.upper()][name]
    except KeyError:
        return None


class GzoneBaseView(MethodView):
    obj = None
    model = None
    query_attr = None
    queryset = None
    form_class = None
    form = None

    def __init__(self, *args, **kwargs):
        super(GzoneBaseView, self).__init__(*args, **kwargs)
        # 注意加上了status的filter
        if self.query_attr is not None:
            self.queryset = getattr(current_room._get_current_object(), self.query_attr).filter_by(status=1)

    def form_valid(self, form):
        pass

    def form_invalid(self, form):
        self.json_data['errors']['form'] = form.errors
        self.status_code = 400
        logger.warning(
            'Form %s validation failed. FormErrors: %s. Code: %s',
            form.__class__.__name__,
            form.errors.items().__str__(),
            self.status_code
        )
        return self.render_to_json_response()


class JsonResponseMixin(object):
    def __init__(self, *args, **kwargs):
        super(JsonResponseMixin, self).__init__(*args, **kwargs)
        self.json_data = dict(errors={})
        self.status_code = 200

    def render_to_json_response(self, status_code=None):
        status_code = status_code or self.status_code
        self.json_data['status_code'] = status_code
        logger.debug(
            'Response: JSON data. Endpoint: %s. code: %s',
            request.endpoint,
            self.status_code
        )
        return jsonify(self.json_data)


# 通告相关view
class AnnouncementView(JsonResponseMixin, GzoneBaseView):
    model = models.Announcement
    query_attr = 'announcements'
    form_class = forms.AnnouncementForm

    def get(self):
        sticky = request.args.get('sticky')
        if sticky is not None:
            return self.get_sticky()
        else:
            return self.get_list()

    @admin_required
    def post(self, id_):
        if id_ is not None:
            return self.set_sticky(id_)
        else:
            return self.add_announcement()

    def get_list(self):
        logger.debug('GET -> get announcements list BEGIN')
        _get_img_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.ANNOUNCEMENT_IMG,
            roomid=current_room.id
        )
        self.json_data['data'] = [{
            'id': ann.id,
            'title': ann.title,
            'content': ann.content,
            'img_url': url_for('.media', filename=_get_img_file_path(ann.img_path), _external=True)
            if ann.img_path is not None else None,
            'create_time': ann.create_time.strftime(DT_TO_STR_FORMAT),
            'is_sticky': ann.id == current_room.sticky_announce_id
        } for ann in self.queryset.order_by(self.model.create_time.desc()).all()]
        self.json_data['data'].sort(key=lambda v: v['is_sticky'], reverse=True)
        self.json_data['success'] = u'获取群通告列表成功'
        logger.debug(
            'GET -> get announcements list END. Code: %s',
            self.status_code
        )

        return self.render_to_json_response()

    def get_sticky(self):
        logger.debug('GET -> get sticky announcement BEGIN')

        _get_img_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.ANNOUNCEMENT_IMG,
            roomid=current_room.id
        )
        ann = current_room.sticky_announcement or self.queryset.first()
        if ann is None:
            self.json_data['errors']['get_sticky_ann'] = u'没有通告数据'
            self.status_code = 404
            logger.warning(
                u'GET -> get sticky announcement FAILED: 没有通告数据. Code: %s',
                self.status_code
            )
        else:
            self.json_data['data'] = dict(
                title=ann.title,
                content=ann.content,
                img_url=url_for('.media', filename=_get_img_file_path(ann.img_path), _external=True)
                if ann.img_path is not None else None,
                create_time=ann.create_time.strftime(DT_TO_STR_FORMAT)
            )
            self.json_data['success'] = u'获取置顶通告成功'
            logger.debug(
                'GET -> get sticky announcement END. Code: %s',
                self.status_code
            )

        return self.render_to_json_response()

    def set_sticky(self, sticky_id):
        logger.debug(
            'POST -> set sticky announcement BEGIN: %s',
            sticky_id
        )
        try:
            current_room.set_sticky_announcement(sticky_id)
        except (NoResultFound, MultipleResultsFound):
            self.json_data['errors']['set_sticky_ann'] = u'不存在该通告项'
            self.status_code = 400
            logger.warning(
                u'POST -> set sticky announcement %s FAILED: 不存在该通告项. Code: %s',
                sticky_id,
                self.status_code
            )
        except SQLAlchemyError as e:
            db.session.rollback()
            self.json_data['errors']['set_sticky_ann'] = u'置顶通告失败'
            # self.json_data['errors']['database'] = str(e)
            self.status_code = 500
            logger.error(
                u'POST -> set sticky announcement %s FAILED: 不存在该通告项. Error: %s. Code: %s',
                sticky_id,
                str(e),
                self.status_code
            )
        else:
            self.json_data['success'] = u'置顶通告成功'
            logger.info(
                'POST -> set sticky announcement END: %s',
                sticky_id
            )
        return self.render_to_json_response()

    def add_announcement(self):
        logger.debug('POST -> adding announcement BEGIN.')
        form = self.form_class()
        if form.validate_on_submit():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        self.obj = self.model(
            title=form.title.data,
            content=form.content.data,
            room=current_room._get_current_object(),
            publisher=current_user._get_current_object(),
        )
        # todo: 处理nginx 413 request entity to large错误
        # eg. client_max_body_size 2M;
        # for flask: MAX_CONTENT_LENGTH
        img = form.image.data
        if img is not None:
            # db中只保存文件名而不保存路径信息
            self.obj.img_path = gzone.media_processor.process_filename(img.filename)

            file_path = gzone.media_processor.get_file_path(
                self.obj.img_path,
                gzone.media_processor.ANNOUNCEMENT_IMG,
                roomid=current_room.id,
                with_root=True
            )
            # todo: 处理文件存储路径和反向路由
            try:
                gzone.media_processor.ensure_dir_exists(file_path)
                img.save(file_path)
            except Exception as e:
                self.json_data['errors']['add_announcement'] = u'发布新通告失败'
                self.status_code = 500
                logger.error(
                    u'POST -> Adding announcement: save img FAILED. Path: %s. Error: %s. Code: %s',
                    file_path,
                    str(e),
                    self.status_code
                )
                return self.render_to_json_response()
        db.session.add(self.obj)
        try:
            db.session.commit()
        except SQLAlchemyError as e:
            db.session.rollback()
            # todo: 数据库提交错误时的消息提示
            self.json_data['errors']['add_announcement'] = u'发布新通告失败'
            # self.json_data['errors']['database'] = str(e)
            self.status_code = 500
            logger.error(
                u'POST -> Adding announcement: save to db FAILED. FormData: %s. Error: %s. Code: %s',
                form.data.items().__str__(),
                str(e),
                self.status_code
            )
        else:
            self.json_data['success'] = u'发布新通告成功'
            logger.info(
                'POST -> adding announcement END: %s',
                self.obj.id
            )
        return self.render_to_json_response()


announcement_view = AnnouncementView.as_view('announcement')
gzone.add_url_rule(
    '/announcement/',
    view_func=announcement_view,
    methods=['GET']
)
gzone.add_url_rule(
    '/announcement/',
    defaults={'id_': None},
    view_func=announcement_view,
    methods=['POST']
)
gzone.add_url_rule(
    '/announcement/<int:id_>',
    view_func=announcement_view,
    methods=['POST']
)


# 公告view
class NoticeView(JsonResponseMixin, GzoneBaseView):
    model = models.Message
    query_attr = 'messages'

    def __init__(self, *args, **kwargs):
        super(NoticeView, self).__init__(*args, **kwargs)
        self.queryset = self.queryset.filter_by(type_=11111)

    def get(self):
        top = request.args.get('top')
        logger.debug(
            'GET -> get notice top list BEGIN. top: %s',
            top
        )

        if top is not None:
            count = current_app.config.get('INDEX_NOTICE_COUNT')
            self.queryset = self.queryset.limit(count)
        try:
            self.json_data['data'] = [{
                'id': m.id,
                'content': m.content,
                'create_time': m.create_time.strftime(DT_TO_STR_FORMAT)
            } for m in self.queryset.all()]
            self.json_data['success'] = u'获取群公告列表成功'
            logger.debug(
                'GET -> get notice top list END. top: %s',
                top
            )
        except SQLAlchemyError as e:
            self.json_data['errors']['get_notice_list'] = u'获取群公告失败'
            self.status_code = 500
            logger.error(
                u'GET -> Getting notice top list FAILED. Error: %s. Code: %s',
                str(e),
                self.status_code
            )

        return self.render_to_json_response()


notice_view = NoticeView.as_view('notice')
gzone.add_url_rule(
    '/notice/',
    view_func=notice_view,
    methods=['GET']
)


# 群友圈相关view
class MomentView(JsonResponseMixin, GzoneBaseView):
    model = models.Moment
    query_attr = 'moments'
    form_class = forms.MomentForm

    def get(self):
        logger.debug('GET -> get moment list BEGIN.')
        _get_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.MOMENT_IMG,
            roomid=current_room.id,
        )
        self.queryset = self.queryset.order_by(self.model.create_time.desc())
        if request.args.get('self') is not None:
            logger.debug('GET -> get moment list(self) BEGIN.')
            self.queryset = self.queryset.filter_by(publisher_openid=current_user.openid)
        page = request.args.get('p', 0)
        page_length = request.args.get(
            'len',
            current_app.config.get('MOMENT_PAGE_LENGTH', 1000)
        )
        self.queryset = self.queryset.offset(page * page_length).limit(page_length)

        self.json_data['data'] = [{
            'id': m.id,
            'content': m.content,
            'images': [
                {
                    'original_url': url_for(
                        '.media',
                        filename=_get_file_path(i.filename, openid=i.owner_openid)[0],
                        _external=True
                    ),
                    'thumb_url': url_for(
                        '.media',
                        filename=_get_file_path(i.filename, openid=i.owner_openid)[1],
                        _external=True
                    ),
                } for i in m.images
            ],
            'create_time': m.create_time.strftime(DT_TO_STR_FORMAT),
            'nickname': m.publisher.nickname,
            'headimgurl': m.publisher.headimgurl
        } for m in self.queryset.all()]
        self.json_data['success'] = u'获取群友圈状态列表成功'
        logger.debug(
            'GET -> get moment list END. Code: %s',
            200
        )

        return self.render_to_json_response()

    def post(self):
        logger.debug('POST -> adding moment BEGIN')
        form = self.form_class()
        if form.validate_on_submit():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def delete(self, id_):
        # todo: 增加删除时对发布时间的判断
        logger.debug(
            'DELETE -> delete moment item BEGIN. id: %s',
            id_
        )
        if current_app.config['ALLOW_ADMIN_DELETION'] and current_user.is_admin:
            self.obj = self.queryset.filter_by(id=id_).first()
            logger.info(
                u'DELETE -> delete moment as ADMIN. NOT CHECK DELETION TIMEUP.'
                u'Roomid: %s. Openid: %s. Code: %s',
                current_room.id,
                current_user.openid,
                self.status_code
            )
        else:
            self.obj = self.queryset.filter_by(id=id_, publisher_openid=current_user.openid).first()
        if self.obj is None:
            self.json_data['errors']['delete'] = u'该群友圈状态不存在'
            self.status_code = 400
            logger.error(
                u'DELETE -> delete moment %s FAILED: 该群友圈状态不存在. Code: %s',
                self.obj.id,
                self.status_code
            )
            return self.render_to_json_response()

        if not (current_app.config['ALLOW_ADMIN_DELETION'] and current_user.is_admin):
            deletion_timeup = current_app.config.get('MOMENT_DELETION_TIMEUP')
            now = datetime.now()
            elapsed = now - self.obj.create_time
            if deletion_timeup is not None and deletion_timeup >= 0 \
                    and (elapsed.days > 0 or elapsed.seconds > deletion_timeup):
                self.json_data['errors']['delete_moment'] = u'只能删除%s秒之内发布的群友圈状态'.format(deletion_timeup)
                self.status_code = 403
                logger.info(
                    u'DELETE -> delete moment %s FAILED: 只能删除%s秒之内发布的群友圈状态. '
                    u'Create time: %s, Delete time: %s. Roomid: %s. Openid: %s. Code: %s',
                    self.obj.id,
                    deletion_timeup,
                    self.obj.create_time,
                    now,
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()

        try:
            self.obj.disable()

            preserve = current_app.config.get('MOMENT_IMG_PRESERVE_ON_DELETE')
            target_ids = (i.id for i in self.obj.images)
            if preserve:
                models.Asset.query.filter(models.Asset.id.in_(target_ids))\
                    .update({'status': 0}, synchronize_session='fetch')
                db.session.commit()
            else:
                models.Asset.query.filter(models.Asset.id.in_(target_ids)) \
                    .delete(synchronize_session='fetch')
                db.session.commit()
                file_path = None
                for img in self.obj.images:
                    file_path, file_path_thumb = gzone.media_processor.get_file_path(
                        img.filename,
                        gzone.media_processor.MOMENT_IMG,
                        roomid=current_room.id,
                        openid=img.owner_openid,
                        with_root=True
                    )
                    os.unlink(file_path)
                    logger.info(
                        u'Deleted moment image on DISK: %s. '
                        u'Roomid: %s. Openid: %s. Owner openid: %s',
                        file_path,
                        current_room.id,
                        current_user.openid,
                        img.owner_openid
                    )
                    os.unlink(file_path_thumb)
                    logger.info(
                        u'Deleted moment image thumbnail on DISK: %s. '
                        u'Roomid: %s. Openid: %s. Owner openid: %s',
                        file_path_thumb,
                        current_room.id,
                        current_user.openid,
                        img.owner_openid
                    )
        except SQLAlchemyError:
            db.session.rollback()
            self.json_data['errors']['delete_moment'] = u'删除群友圈状态失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'DELETE -> delete moment %s FAILED: 删除群友圈状态失败. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Owner openid: %s. Code: %s',
                self.obj.id,
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.obj.publisher_openid,
                self.status_code
            )
        except Exception:
            self.json_data['errors']['delete_moment_photo'] = u'删除群友圈状态(图片)失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> Deleting moment image on DISK FAILED. '
                u'Path: %s. Error: %s. TB: %s. Roomid: %s. Openid: %s. Owner openid: %s. '
                u'Code: %s',
                file_path,
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.obj.publisher_openid,
                self.status_code
            )
            return self.render_to_json_response()
        else:
            self.json_data['success'] = u'删除群友圈状态成功'
            logger.info(
                'DELETE -> delete moment END: %s. '
                'Roomid: %s. Openid: %s. Owner openid: %s. Code: %s',
                self.obj.id,
                current_room.id,
                current_user.openid,
                self.obj.publisher_openid,
                self.status_code
            )
        return self.render_to_json_response()

    def form_valid(self, form):
        self.obj = self.model(
            content=form.content.data,
            room=current_room._get_current_object(),
            publisher=current_user._get_current_object()
        )

        images = request.files.getlist('images')
        for img in images:
            if not img.filename:
                continue
            ext = os.path.splitext(img.filename)[1].lstrip('.').upper()
            if ext not in current_app.config['ALLOWED_UPLOAD_IMG_EXT']:
                self.json_data['errors']['add_moment_image'] = u'请选择正确的图片类型: {}' \
                    .format(current_app.config['ALLOWED_UPLOAD_IMG_EXT'])
                self.status_code = 400
                logger.error(
                    u'POST -> Adding moment images FAILED: Wrong type. %s '
                    u'Roomid: %s. Openid: %s. Code: %s',
                    ext,
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()
        saved = []
        try:
            for img in images:
                if not img.filename:
                    continue

                photo = models.Asset(
                    type_=models.Asset.MOMENT_IMG,
                    owner_openid=current_user.openid,
                    filename=gzone.media_processor.process_filename(img.filename)
                )
                file_path, file_path_thumb = gzone.media_processor.get_file_path(
                    photo.filename,
                    gzone.media_processor.MOMENT_IMG,
                    roomid=current_room.id,
                    openid=current_user.openid,
                    with_root=True
                )
                gzone.media_processor.ensure_dir_exists(file_path)
                img_saved = 0
                img.save(file_path)
                img_saved = 1
                logger.info(
                    u'saved moment image to: %s. Roomid: %s. Openid: %s',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
                gzone.media_processor.ensure_dir_exists(file_path_thumb)
                gzone.media_processor.generate_thumbnail(img).save(file_path_thumb)
                logger.info(
                    u'saved moment image thumbnail to: %s. Roomid: %s. Openid: %s',
                    file_path_thumb,
                    current_room.id,
                    current_user.openid
                )
                saved.append((photo, file_path, file_path_thumb))
                # db.session.add(photo)
                self.obj.images.append(photo)
            db.session.add(self.obj)
            db.session.commit()
        except SQLAlchemyError:
            db.session.rollback()
            # todo: 数据库提交错误时的处理
            self.json_data['errors']['add_moment'] = u'发布新群友圈状态失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> Adding moment: save to db FAILED. FormData: %s. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                form.data.items().__str__(),
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            for _, p, p_t in saved:
                os.unlink(p)
                logger.info(
                    'Unlinked moment image: %s. Roomid: %s. Openid: %s',
                    p,
                    current_room.id,
                    current_user.openid
                )
                os.unlink(p_t)
                logger.info(
                    'Unlinked moment image thumbnail: %s. Roomid: %s. Openid: %s',
                    p_t,
                    current_room.id,
                    current_user.openid
                )
        except Exception:
            self.json_data['errors']['add_moment_image'] = u'保存群友圈状态(图片)失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> Adding moment image: save img to DISK FAILED. Path: %s. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                file_path,
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            if img_saved:
                os.unlink(file_path)
                logger.info(
                    'Unlinked moment image: %s. Roomid: %s. Openid: %s',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
            for _, p, p_t in saved:
                os.unlink(p)
                logger.info(
                    'Unlinked moment image: %s. Roomid: %s. Openid: %s',
                    p,
                    current_room.id,
                    current_user.openid
                )
                os.unlink(p_t)
                logger.info(
                    'Unlinked moment image thumbnail: %s. Roomid: %s. Openid: %s',
                    p_t,
                    current_room.id,
                    current_user.openid
                )
        else:
            self.json_data['success'] = u'发布新群友圈状态成功'
            logger.info(
                'POST -> adding moment END: %s '
                ' Roomid: %s. Openid: %s. Code: %s',
                self.obj.id,
                current_room.id,
                current_user.openid,
                self.status_code
            )
        return self.render_to_json_response()


moment_view = MomentView.as_view('moment')
gzone.add_url_rule(
    '/moment/',
    view_func=moment_view,
    methods=['GET']
)
gzone.add_url_rule(
    '/moment/',
    view_func=moment_view,
    methods=['POST']
)
gzone.add_url_rule(
    '/moment/<int:id_>',
    view_func=moment_view,
    methods=['DELETE']
)


class ContactView(JsonResponseMixin, GzoneBaseView):
    model = models.Account
    # 跟Announcement, Moment稍有不同
    query_attr = 'accounts'
    form_class = forms.AccountForm

    def get(self, id_):
        if id_ is None:
            show_self = request.args.get('self')
            if show_self is not None:
                return self.get_self()
            return self.get_list()
        return self.get_detail(id_)

    def post(self):
        logger.debug('POST -> edit contact(self) BEGIN')
        try:
            self.obj = self.queryset.filter_by(account_openid=current_user.openid).one().account
        except (NoResultFound, MultipleResultsFound):
            self.json_data['errors']['impossible'] = u'当前用户不存在@@'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> edit contact FAILED: 当前用户不存在@@. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            return self.render_to_json_response()

        self.form = self.form_class()
        if self.form.validate_on_submit():
            return self.form_valid(self.form)
        else:
            return self.form_invalid(self.form)

    def get_self(self):
        logger.debug('GET -> get contact(self) BEGIN')
        _get_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.ACCOUNT_BANNER_IMG,
        )
        try:
            self.obj = self.queryset.filter_by(account_openid=current_user.openid).one().account
        except (NoResultFound, MultipleResultsFound) as e:
            self.json_data['errors']['impossible'] = u'当前用户不存在@@'
            self.status_code = 500
            logger.error(
                u'GET -> get contact(self) FAILED: 当前用户不存在@@. Code: %s',
                self.status_code
            )
        else:
            self.json_data['data'] = {
                'openid': self.obj.id,
                'nickname': self.obj.nickname,
                'headimgurl': self.obj.headimgurl,
                'email': self.obj.email,
                'tel': self.obj.tel,
                'company': self.obj.company,
                'title': self.obj.title,
                'desc': self.obj.desc,
                'banner_url': None if self.obj.banner is None else url_for(
                    '.media',
                    filename=_get_file_path(self.obj.banner.filename),
                    _external=True
                )
            }
            self.json_data['success'] = u'获取当前用户通讯录详细信息成功'
            logger.debug(
                'GET -> get contact(self) END. Code: %s',
                self.status_code
            )
        return self.render_to_json_response()

    def get_list(self):
        logger.debug('GET -> get contact list BEGIN')
        self.json_data['data'] = [{
            'openid': ra.account.id,
            'nickname': ra.account.nickname,
            'headimgurl': ra.account.headimgurl,
            'email': ra.account.email,
            'tel': ra.account.tel,
            'company': ra.account.company,
            'title': ra.account.title,
            'desc': ra.account.desc,
            'message_count': ra.message_count,
            'signin_count': ra.signin_count,
            'succ_signin_count': ra.successive_signin_count,
            'bonus_count': 0
        } for ra in self.queryset.all()]
        self.json_data['success'] = u'获取群通讯录列表成功'
        logger.debug(
            'GET -> get contact list END. Code: %s',
            self.status_code
        )

        return self.render_to_json_response()

    def get_detail(self, id_):
        logger.debug(
            'GET -> get contact detail BEGIN. ID: %s',
            id_
        )
        try:
            account = self.queryset.join(models.Account)\
                .filter(models.Account.id == id_).one().account
        except (NoResultFound, MultipleResultsFound):
            self.json_data['errors']['get_contact_detail'] = u'指定用户不存在'
            self.status_code = 404
            logger.error(
                u'GET -> get contact detail %s FAILED: 指定用户不存在. Code: %s',
                id_,
                self.status_code
            )
        else:
            self.json_data['data'] = {
                'openid': account.id,
                'nickname': account.nickname,
                'headimgurl': account.headimgurl,
                'email': account.email,
                'tel': account.tel,
                'company': account.company,
                'title': account.title,
                'desc': account.desc,
            }
            self.json_data['success'] = u'获取通讯录详细信息成功'
            logger.debug(
                'GET -> get contact detail END. ID: %s. Code: %s',
                id_,
                self.status_code
            )

        return self.render_to_json_response()

    def form_valid(self, form):
        # form.populate_obj(self.obj)
        if form.email.data:
            self.obj.email = form.email.data
        if form.tel.data:
            self.obj.tel = form.tel.data
        if form.company.data:
            self.obj.company = form.company.data
        if form.title.data:
            self.obj.title = form.title.data
        if form.desc.data:
            self.obj.desc = form.desc.data
        banner_img = form.banner.data
        file_path, banner_fn_orig = None, None
        try:
            if banner_img is not None:
                ext = os.path.splitext(banner_img.filename)[1].lstrip('.').upper()
                if ext not in current_app.config['ALLOWED_UPLOAD_IMG_EXT']:
                    self.json_data['errors']['add_album_photo'] = u'请选择正确的图片类型: {}' \
                        .format(current_app.config['ALLOWED_UPLOAD_IMG_EXT'])
                    self.status_code = 400
                    logger.error(
                        u'POST -> Adding photo to album FAILED: Wrong type. %s '
                        u'Roomid: %s. Openid: %s. Code: %s',
                        ext,
                        current_room.id,
                        current_user.openid,
                        self.status_code
                    )
                    return self.render_to_json_response()

                banner_fn = '{}.{}'.format(current_user.openid, ext)
                banner_fn_orig = self.obj.banner_asset.filename
                file_path = gzone.media_processor.get_file_path(
                    banner_fn,
                    gzone.media_processor.ACCOUNT_BANNER_IMG,
                    with_root=True
                )
                if banner_fn_orig is None:
                    gzone.media_processor.ensure_dir_exists(file_path)
                else:
                    banner_fn_orig = gzone.media_processor.get_file_path(
                        banner_fn_orig,
                        gzone.media_processor.ACCOUNT_BANNER_IMG,
                        with_root=True
                    )
                    banner_fn_orig_bak = banner_fn_orig + '.bak'
                    os.rename(banner_fn_orig, banner_fn_orig_bak)
                    logger.info(
                        u'Edit contact detail, BACKUPED original banner img: %s -> %s '
                        u'Roomid: %s.  Openid: %s.',
                        banner_fn_orig,
                        banner_fn_orig_bak,
                        current_room.id,
                        current_user.openid
                    )
                banner_img.save(file_path)
                logger.info(
                    u'Edit contact detail, SAVED banner img: %s '
                    u'Roomid: %s.  Openid: %s.',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
                self.obj.banner_asset.filename = banner_fn
            db.session.add(self.obj)
            db.session.commit()
        except SQLAlchemyError:
            db.session.rollback()
            self.json_data['errors']['update_contact'] = u'修改通讯录信息失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> edit contact detail FAILED. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            if file_path is not None:
                os.unlink(file_path)
                logger.info(
                    u'Edit contact detail, DELETED banner img: %s '
                    u'Roomid: %s.  Openid: %s.',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
            if banner_fn_orig is not None:
                os.rename(banner_fn_orig_bak, banner_fn_orig)
                logger.info(
                    u'Edit contact detail, ROLLED BACK original banner img: %s -> %s '
                    u'Roomid: %s.  Openid: %s.',
                    banner_fn_orig_bak,
                    banner_fn_orig,
                    current_room.id,
                    current_user.openid
                )
        except Exception:
            self.json_data['errors']['update_contact'] = u'修改通讯录信息失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> edit contact detail: save banner img to DISK FAILED. Path: %s. '
                u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                file_path,
                exc_value,
                traceback.format_tb(exc_tb),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            if banner_fn_orig is not None:
                os.rename(banner_fn_orig_bak, banner_fn_orig)
                logger.info(
                    u'Edit contact detail, ROLLED BACK original banner img: %s -> %s '
                    u'Roomid: %s.  Openid: %s.',
                    banner_fn_orig_bak,
                    banner_fn_orig,
                    current_room.id,
                    current_user.openid
                )
        else:
            if banner_fn_orig is not None:
                os.unlink(banner_fn_orig_bak)
                logger.info(
                    u'Edit contact detail, DELETED original banner img backup: %s '
                    u'Roomid: %s.  Openid: %s.',
                    banner_fn_orig_bak,
                    current_room.id,
                    current_user.openid
                )
            self.json_data['success'] = u'修改个人信息成功'
            logger.info(
                'POST -> edit contact detail END: %s',
                self.obj.openid
            )

        return self.render_to_json_response()


contact_view = ContactView.as_view('contact')
gzone.add_url_rule(
    '/contact/',
    defaults={'id_': None},
    view_func=contact_view,
    methods=['GET']
)
gzone.add_url_rule(
    '/contact/',
    view_func=contact_view,
    methods=['POST']
)
gzone.add_url_rule(
    '/contact/<id_>',
    view_func=contact_view,
    methods=['GET', 'POST']
)


class AlbumView(JsonResponseMixin, GzoneBaseView):
    model = models.Asset
    query_attr = 'photos'
    form_class = forms.AlbumForm

    def get(self):
        # todo: 按日期返回group数据
        logger.debug('GET -> get photo list BEGIN')
        _get_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.GALBUM,
            roomid=current_room.id,
        )
        self.queryset = self.queryset.order_by(self.model.create_time.desc())
        group_img_queryset = current_room.group_images\
            .order_by(models.Message.create_time.desc())
        if request.args.get('self') is not None:
            logger.debug('GET -> get photo list(self) BEGIN')
            # 为了避免重复filter status和order_by,这里没有使用current_user.photos
            self.queryset = self.queryset.filter_by(owner_openid=current_user.openid)
            # dummy filter
            # todo: add correct filter after wxid <=> openid
            group_img_queryset = group_img_queryset.filter_by(id=0)

        self.json_data['data'] = [{
            'id': p.id,
            'img_url': url_for(
                '.media',
                filename=_get_file_path(p.filename, openid=p.owner_openid)[0],
                _external=True
            ),
            'img_thumb_url':url_for(
                '.media',
                filename=_get_file_path(p.filename, openid=p.owner_openid)[1],
                _external=True
            ),
            'group_image': False,
            'create_time': p.create_time.strftime(DT_TO_STR_FORMAT),
            'owner_nickname': p.owner.nickname
        } for p in self.queryset.all()]

        if request.args.get('exclude_group') is None:
            logger.debug('GET -> get group album list. LOADING GROUP IMAGES.')
            _get_file_path = partial(
                gzone.media_processor.get_file_path,
                type_=gzone.media_processor.GROUP_IMG,
                roomid=current_room.id,
            )
            # todo: group image thumbnails
            # todo: owner_nickname
            self.json_data['data'] += [{
                'id': 'g_' + str(p.id),
                'img_url': url_for(
                    '.media',
                    filename=_get_file_path(p.filepath),
                    _external=True
                ),
                'img_thumb_url': url_for(
                    '.media',
                    filename=_get_file_path(p.filepath),
                    _external=True
                ),
                'group_image': True,
                'create_time': p.create_time.strftime(DT_TO_STR_FORMAT),
                'owner_nickname': None
            } for p in group_img_queryset.all()]
        self.json_data['success'] = u'获取群相册列表成功'
        logger.debug(
            'GET -> get group album list END. Code: %s',
            self.status_code
        )

        return self.render_to_json_response()

    def post(self):
        logger.debug(
            'POST -> uploading new photos to album BEGIN. Room: %s. Openid: %s',
            current_room.id,
            current_user.openid
        )
        self.form = form = self.form_class()
        if form.validate_on_submit():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def delete(self):
        target_ids_all = request.form.getlist('targets[]')
        target_ids = set(int(t) for t in target_ids_all if not t.startswith('g_'))
        target_ids_g = set(int(t.lstrip('g_')) for t in target_ids_all if t.startswith('g_'))
        logger.debug(
            'DELETE -> delete album photos BEGIN. Targets: %s. Roomid: %s. Openid: %s',
            str(target_ids),
            current_room.id,
            current_user.openid
        )
        if not target_ids and not target_ids_g:
            self.json_data['errors']['delete_album_photo'] = u'没有指定删除照片'
            self.status_code = 204
            logger.info(
                'POST -> Deleting album photo END. Nothing deleted as target is empty. '
                'IDs: %s. Roomid: %s. Openid: %s',
                str(target_ids),
                current_room.id,
                current_user.openid
            )
            return self.render_to_json_response()

        if current_app.config['ALLOW_ADMIN_DELETION'] and current_user.is_admin:
            to_delete = self.queryset.filter(self.model.id.in_(target_ids)).all()
            # todo: change group images filter logic
            # to_delete_g = current_room.group_images.filter(models.Message.id.in_(target_ids_g)).all()
            to_delete_g = current_room.group_images.filter_by(id=0)\
                .filter(models.Message.id.in_(target_ids_g)).all()
            logger.info(
                u'DELETE -> delete album photo as ADMIN. '
                u'Roomid: %s. Openid: %s. Code: %s',
                current_room.id,
                current_user.openid,
                self.status_code
            )
        else:
            to_delete = self.queryset.filter_by(owner_openid=current_user.openid).\
                filter(self.model.id.in_(target_ids)).all()
            # dummy filter
            # todo: add correct filter after wxid <=> openid
            to_delete_g = current_room.group_images.filter_by(id=0)\
                .filter(models.Message.id.in_(target_ids_g)).all()
        to_delete_ids = set(p.id for p in to_delete)
        to_delete_ids_g = set(p.id for p in to_delete_g)
        not_owned_ids = target_ids - to_delete_ids
        not_owned_ids_g = target_ids_g - to_delete_ids_g
        if not_owned_ids or not_owned_ids_g:
            self.json_data['errors']['delete_album_photo'] = u'只能删除自己照片或包含非法数据'
            self.status_code = 403
            logger.info(
                u'DELETE -> delete album photo FAILED: 只能删除自己照片或包含非法数据. '
                u'Illegal ids: %s. Roomid: %s. Openid: %s. Code: %s',
                str(not_owned_ids),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            return self.render_to_json_response()

        preserve = current_app.config.get('ALBUM_PHOTO_PRESERVE_ON_DELETE')
        if preserve:
            try:
                self.queryset.filter(self.model.id.in_(target_ids))\
                    .update({'status': 0}, synchronize_session='fetch')
                # todo: add group image deletion logic
                db.session.commit()
            except SQLAlchemyError as e:
                db.session.rollback()
                self.json_data['errors']['delete_album_photo'] = u'删除群相册照片失败'
                self.status_code = 500
                logger.error(
                    u'DELETE -> delete album photo in DB FAILED.(Updating status) '
                    u'Target ids: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    str(target_ids),
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
            else:
                # todo: add group image deletion logic
                self.json_data['success'] = u'删除群相册照片成功'
                logger.info(
                    'DELETE -> Deleting album photo END.(DB entries and files are preserved) '
                    'IDs: %s. Roomid: %s. Openid: %s',
                    str(target_ids),
                    current_room.id,
                    current_user.openid
                )
            return self.render_to_json_response()
        else:
            try:
                self.queryset.filter(self.model.id.in_(target_ids))\
                    .delete(synchronize_session='fetch')
                db.session.commit()
                logger.info(
                    u'Deleted album photo DB: %s. Roomid: %s. Openid: %s',
                    str(target_ids),
                    current_room.id,
                    current_user.openid
                )
            except SQLAlchemyError as e:
                db.session.rollback()
                self.json_data['errors']['delete_album_photo'] = u'删除群相册照片失败'
                self.status_code = 500
                logger.error(
                    u'DELETE -> delete album photo in DB FAILED.(Deleting entries) '
                    u'Target ids: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    str(target_ids),
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()
            try:
                file_path = None
                for p in to_delete:
                    file_path, file_path_thumb = gzone.media_processor.get_file_path(
                        p.filename,
                        gzone.media_processor.GALBUM,
                        roomid=current_room.id,
                        openid=p.owner_openid,
                        with_root=True
                    )
                    os.unlink(file_path)
                    logger.info(
                        u'Deleted album photo on DISK: %s. '
                        u'Roomid: %s. Openid: %s. Owner openid: %s',
                        file_path,
                        current_room.id,
                        current_user.openid,
                        p.owner_openid
                    )
                    os.unlink(file_path_thumb)
                    logger.info(
                        u'Deleted album photo thumbnail on DISK: %s. '
                        u'Roomid: %s. Openid: %s. Owner openid: %s',
                        file_path_thumb,
                        current_room.id,
                        current_user.openid,
                        p.owner_openid
                    )
            except Exception as e:
                self.json_data['errors']['delete_album_photo'] = u'删除群相册照片失败'
                self.status_code = 500
                logger.error(
                    u'POST -> Deleting album photo on DISK FAILED. '
                    u'Path: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    file_path,
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()
            self.json_data['success'] = u'删除群相册照片成功'
            logger.info(
                'POST -> Deleting album photo END. IDs: %s. Roomid: %s. Openid: %s',
                str(target_ids),
                current_room.id,
                current_user.openid
            )
            return self.render_to_json_response()

    def form_valid(self, form):
        images = request.files.getlist('images')
        for img in images:
            if not img.filename:
                continue
            ext = os.path.splitext(img.filename)[1].lstrip('.').upper()
            if ext not in current_app.config['ALLOWED_UPLOAD_IMG_EXT']:
                self.json_data['errors']['add_album_photo'] = u'请选择正确的图片类型: {}'\
                    .format(current_app.config['ALLOWED_UPLOAD_IMG_EXT'])
                self.status_code = 400
                logger.error(
                    u'POST -> Adding photo to album FAILED: Wrong type. %s '
                    u'Roomid: %s. Openid: %s. Code: %s',
                    ext,
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()

        saved = []
        for img in images:
            if not img.filename:
                continue

            photo = self.model(
                type_=self.model.ALBUM_PHOTO,
                owner_openid=current_user.openid,
                room_id=current_room.id,
                filename=gzone.media_processor.process_filename(img.filename)
            )
            file_path, file_path_thumb = gzone.media_processor.get_file_path(
                photo.filename,
                gzone.media_processor.GALBUM,
                roomid=current_room.id,
                openid=current_user.openid,
                with_root=True
            )
            # todo: 处理文件存储路径和反向路由
            try:
                gzone.media_processor.ensure_dir_exists(file_path)
                img_saved = 0
                img.save(file_path)
                img_saved = 1
                logger.info(
                    u'saved album photo to: %s. Roomid: %s. Openid: %s',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
                gzone.media_processor.ensure_dir_exists(file_path_thumb)
                gzone.media_processor.generate_thumbnail(img).save(file_path_thumb)
                logger.info(
                    u'saved album photo thumbnail to: %s. Roomid: %s. Openid: %s',
                    file_path_thumb,
                    current_room.id,
                    current_user.openid
                )
            except Exception as e:
                self.json_data['errors']['add_album_photo'] = u'保存群相册相片失败'
                self.status_code = 500
                exc_type, exc_value, exc_tb = sys.exc_info()
                logger.error(
                    u'POST -> Adding photo to album: save img FAILED. Path: %s. '
                    u'Error: %s. TB: %s. Roomid: %s. Openid: %s. Code: %s',
                    file_path,
                    exc_value,
                    traceback.format_tb(exc_tb),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                if img_saved:
                    os.unlink(file_path)
                    logger.info(
                        'Unlinked album photo: %s. Roomid: %s. Openid: %s',
                        file_path,
                        current_room.id,
                        current_user.openid
                    )
                for _, p, p_t in saved:
                    os.unlink(p)
                    logger.info(
                        'Unlinked album photo: %s. Roomid: %s. Openid: %s',
                        p,
                        current_room.id,
                        current_user.openid
                    )
                    os.unlink(p_t)
                    logger.info(
                        'Unlinked album photo thumbnail: %s. Roomid: %s. Openid: %s',
                        p_t,
                        current_room.id,
                        current_user.openid
                    )
                return self.render_to_json_response()
            else:
                saved.append((photo, file_path, file_path_thumb))
                db.session.add(photo)
        try:
            db.session.commit()
        except SQLAlchemyError as e:
            db.session.rollback()
            self.json_data['errors']['add_album_photo'] = u'添加群相册相片失败'
            self.status_code = 500
            logger.error(
                u'POST -> Adding photo to alubm: save to db FAILED. '
                u'Error: %s. Roomid: %s. Openid: %s. Code: %s.',
                str(e),
                current_room.id,
                current_user.openid,
                self.status_code
            )
        else:
            self.json_data['success'] = u'添加群相册照片成功'
            logger.info(
                'POST -> uploading new photos to album END. IDs: %s. Roomid: %s. Openid: %s. Code: %s',
                ', '.join(str(s[0].id) for s in saved),
                current_room.id,
                current_user.openid,
                self.status_code
            )
        return self.render_to_json_response()


album_view = AlbumView.as_view('album')
gzone.add_url_rule(
    '/album/',
    view_func=album_view,
    methods=['GET', 'POST', 'DELETE']
)


class GroupFileView(JsonResponseMixin, GzoneBaseView):
    model = models.Asset
    query_attr = 'files'
    form_class = forms.GroupFileForm

    def get(self):
        # todo: 按日期返回group数据
        logger.debug('GET -> get group file list BEGIN')
        _get_file_path = partial(
            gzone.media_processor.get_file_path,
            type_=gzone.media_processor.GFILE,
            roomid=current_room.id,
        )
        self.queryset = self.queryset.order_by(self.model.create_time.desc())
        if request.args.get('self') is not None:
            logger.debug('GET -> get group file list(self) BEGIN')
            # 为了避免重复filter status和order_by,这里没有使用current_user.photos
            self.queryset = self.queryset.filter_by(owner_openid=current_user.openid)
        self.json_data['data'] = [{
            'id': f.id,
            'file_url': url_for(
                '.media',
                filename=_get_file_path(f.filename, openid=f.owner_openid),
                _external=True
            ),
            'filename': f.original_filename,
            'filesize': None if f.filesize is None
            else hf.format_size(f.filesize, binary=True),
            'create_time': f.create_time.strftime(DT_TO_STR_FORMAT),
            'is_sticky': f.sticky,
            'owner_nickname': f.owner.nickname
        } for f in self.queryset.all()]
        self.json_data['data'].sort(key=lambda v: v['is_sticky'], reverse=True)
        self.json_data['success'] = u'获取群文件列表成功'
        logger.debug(
            'GET -> get group file list END. Code: %s',
            self.status_code
        )

        return self.render_to_json_response()

    def post(self, id_):
        if id_ is None:
            return self.upload_group_file()
        else:
            return self.toggle_sticky(id_)

    def delete(self):
        target_ids = set(request.form.getlist('targets[]', int))
        logger.debug(
            'DELETE -> delete group files BEGIN. Targets: %s. Roomid: %s. Openid: %s',
            str(target_ids),
            current_room.id,
            current_user.openid
        )
        if not target_ids:
            self.json_data['errors']['delete_group_file'] = u'没有指定删除群文件'
            self.status_code = 204
            logger.info(
                'POST -> Deleting group file END. Nothing deleted as target is empty. '
                'IDs: %s. Roomid: %s. Openid: %s',
                str(target_ids),
                current_room.id,
                current_user.openid
            )
            return self.render_to_json_response()

        if current_app.config['ALLOW_ADMIN_DELETION'] and current_user.is_admin:
            to_delete = self.queryset.filter(self.model.id.in_(target_ids)).all()
            logger.info(
                u'DELETE -> delete group file as ADMIN. '
                u'Roomid: %s. Openid: %s. Code: %s',
                current_room.id,
                current_user.openid,
                self.status_code
            )
        else:
            to_delete = self.queryset.filter_by(owner_openid=current_user.openid)\
                .filter(self.model.id.in_(target_ids)).all()
        to_delete_ids = set(f.id for f in to_delete)
        not_owned_ids = target_ids - to_delete_ids
        if not_owned_ids:
            self.json_data['errors']['delete_group_file'] = u'只能删除自己的文件或包含非法数据'
            self.status_code = 403
            logger.info(
                u'DELETE -> delete group files FAILED: 只能删除自己的文件或包含非法数据. '
                u'Illegal ids: %s. Roomid: %s. Openid: %s. Code: %s',
                str(not_owned_ids),
                current_room.id,
                current_user.openid,
                self.status_code
            )
            return self.render_to_json_response()

        preserve = current_app.config.get('GROUP_FILE_PRESERVE_ON_DELETE')
        if preserve:
            try:
                self.queryset.filter(self.model.id.in_(target_ids)) \
                    .update({'status': 0}, synchronize_session='fetch')
                db.session.commit()
            except SQLAlchemyError as e:
                db.session.rollback()
                self.json_data['errors']['delete_group_file'] = u'删除群文件失败'
                self.status_code = 500
                logger.error(
                    u'DELETE -> delete group files in DB FAILED.(Updating status) '
                    u'Target ids: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    str(target_ids),
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
            else:
                self.json_data['success'] = u'删除群文件成功'
                logger.info(
                    'DELETE -> Deleting group file END.(DB entries and files are preserved) '
                    'IDs: %s. Roomid: %s. Openid: %s',
                    str(target_ids),
                    current_room.id,
                    current_user.openid
                )
            return self.render_to_json_response()
        else:
            try:
                self.queryset.filter(self.model.id.in_(target_ids)) \
                    .delete(synchronize_session='fetch')
                db.session.commit()
                logger.info(
                    u'Deleted group file DB: %s. Roomid: %s. Openid: %s',
                    str(target_ids),
                    current_room.id,
                    current_user.openid
                )
            except SQLAlchemyError as e:
                db.session.rollback()
                self.json_data['errors']['delete_group_file'] = u'删除群文件失败'
                self.status_code = 500
                logger.error(
                    u'DELETE -> delete group file in DB FAILED.(Deleting entries) '
                    u'Target ids: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    str(target_ids),
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()
            try:
                file_path = None
                for f in to_delete:
                    file_path = gzone.media_processor.get_file_path(
                        f.filename,
                        gzone.media_processor.GFILE,
                        roomid=current_room.id,
                        openid=f.owner_openid,
                        with_root=True
                    )
                    os.unlink(file_path)
                    logger.info(
                        u'Deleted group file on DISK: %s. '
                        u'Roomid: %s. Openid: %s. Owner openid: %s',
                        file_path,
                        current_room.id,
                        current_user.openid,
                        f.owner_openid
                    )
            except Exception as e:
                self.json_data['errors']['delete_group_file'] = u'删除群文件失败'
                self.status_code = 500
                logger.error(
                    u'POST -> Deleting group file on DISK FAILED. '
                    u'Path: %s. Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    file_path,
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()
            self.json_data['success'] = u'删除群文件成功'
            logger.info(
                'POST -> Deleting group file END. IDs: %s. Roomid: %s. Openid: %s',
                str(target_ids),
                current_room.id,
                current_user.openid
            )
            return self.render_to_json_response()

    def form_valid(self, form):
        files = request.files.getlist('files')
        for u_file in files:
            if not u_file.filename:
                continue
            ext = os.path.splitext(u_file.filename)[1].lstrip('.').upper()
            if ext not in current_app.config['ALLOWED_UPLOAD_FILE_EXT']:
                self.json_data['errors']['add_group_file'] = u'请选择正确的文件类型: {}'\
                    .format(current_app.config['ALLOWED_UPLOAD_FILE_EXT'])
                self.status_code = 400
                logger.error(
                    u'POST -> Adding group file FAILED: Wrong type. %s'
                    u'Roomid: %s. Openid: %s. Code: %s',
                    ext,
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                return self.render_to_json_response()

        saved = []
        for u_file in files:
            if not u_file.filename:
                continue
            f_obj = self.model(
                type_=self.model.GROUP_FILE,
                owner_openid=current_user.openid,
                room_id=current_room.id,
                filename=gzone.media_processor.process_filename(u_file.filename),
                original_filename=u_file.filename
            )
            file_path = gzone.media_processor.get_file_path(
                f_obj.filename,
                gzone.media_processor.GFILE,
                roomid=current_room.id,
                openid=current_user.openid,
                with_root=True
            )
            # todo: 处理文件存储路径和反向路由
            try:
                gzone.media_processor.ensure_dir_exists(file_path)
                u_file.save(file_path)
                f_obj.filesize = os.path.getsize(file_path)
            except Exception as e:
                self.json_data['errors']['add_group_file'] = u'保存群文件失败'
                self.status_code = 500
                logger.error(
                    u'POST -> Adding group files: save file to disk FAILED. Path: %s. '
                    u'Error: %s. Roomid: %s. Openid: %s. Code: %s',
                    file_path,
                    str(e),
                    current_room.id,
                    current_user.openid,
                    self.status_code
                )
                for _, p in saved:
                    os.unlink(p)
                    logger.info(
                        'Unlinked group file: %s. Roomid: %s. Openid: %s',
                        p,
                        current_room.id,
                        current_user.openid
                    )
                return self.render_to_json_response()
            else:
                saved.append((f_obj, file_path))
                db.session.add(f_obj)
                logger.info(
                    u'saved group file to: %s. Roomid: %s. Openid: %s',
                    file_path,
                    current_room.id,
                    current_user.openid
                )
        try:
            db.session.commit()
        except SQLAlchemyError as e:
            db.session.rollback()
            self.json_data['errors']['add_group_file'] = u'添加群文件失败'
            self.status_code = 500
            logger.error(
                u'POST -> Adding group file: save to db FAILED. '
                u'Error: %s. Roomid: %s. Openid: %s. Code: %s.',
                str(e),
                current_room.id,
                current_user.openid,
                self.status_code
            )
        else:
            self.json_data['success'] = u'添加群文件成功'
            logger.info(
                'POST -> uploading new group file END. IDs: %s. Roomid: %s. Openid: %s. Code: %s',
                ', '.join(str(s[0].id) for s in saved),
                current_room.id,
                current_user.openid,
                self.status_code
            )
        return self.render_to_json_response()

    def upload_group_file(self):
        logger.debug(
            'POST -> uploading new group files BEGIN. Room: %s. Openid: %s',
            current_room.id,
            current_user.openid
        )
        self.form = form = self.form_class()
        if form.validate_on_submit():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    @admin_required
    def toggle_sticky(self, sticky_id):
        logger.debug(
            'POST -> toggle sticky group file BEGIN: %s',
            sticky_id
        )
        try:
            self.queryset.filter_by(id=sticky_id).one().toggle_sticky()
        except (NoResultFound, MultipleResultsFound):
            self.json_data['errors']['toggle_sticky_gfile'] = u'不存在该群文件'
            self.status_code = 400
            logger.warning(
                u'POST -> toggle sticky group file %s FAILED: 不存在该群文件. Code: %s',
                sticky_id,
                self.status_code
            )
        except SQLAlchemyError:
            db.session.rollback()
            self.json_data['errors']['toggle_sticky_gfile'] = u'设置/取消置顶群文件失败'
            self.status_code = 500
            exc_type, exc_value, exc_tb = sys.exc_info()
            logger.error(
                u'POST -> toggle sticky group file %s FAILED. Error: %s. TB: %s. Code: %s',
                sticky_id,
                traceback.format_tb(exc_tb),
                exc_value,
                self.status_code
            )
        else:
            self.json_data['success'] = u'设置/取消置顶群文件成功'
            logger.info(
                'POST -> toggle sticky group file END: %s',
                sticky_id
            )
        return self.render_to_json_response()

group_file_view = GroupFileView.as_view('gfile')
gzone.add_url_rule(
    '/gfile/',
    view_func=group_file_view,
    methods=['GET', 'DELETE']
)
gzone.add_url_rule(
    '/gfile/',
    defaults={'id_': None},
    view_func=group_file_view,
    methods=['POST']
)
gzone.add_url_rule(
    '/gfile/<int:id_>',
    view_func=group_file_view,
    methods=['POST']
)


class LoginView(JsonResponseMixin, MethodView):
    def post(self):
        # todo: 增加csrf验证
        password = request.form.get('password')
        owner_wxid = current_app.redis_interface.get_room_owner(current_room.id)
        logger.debug(
            'POST -> admin login BEGIN. Password: %s, Room owner wxid: %s',
            password,
            owner_wxid
        )
        if owner_wxid is None:
            # todo: 微信群没有被管理情况下错误处理
            # 　abort(500)
            self.json_data['errors']['impossible'] = u'该微信群没有被购买'
            self.status_code = 500
            logger.error(
                u'POST -> admin login FAILED: 该微信群没有被购买. Code: %s',
                self.status_code
            )
            return self.render_to_json_response()
        try:
            owner = models.Admin.query.filter(models.Admin.wxid == owner_wxid).one()
        except (NoResultFound, MultipleResultsFound) as e:
            # todo: 数据不一致处理
            #abort(500)
            self.json_data['errors']['impossible'] = u'内部数据不一致'
            self.status_code = 500
            logger.error(
                u'POST -> admin login FAILED: 内部数据不一致. DB error: %s. Code: %s',
                str(e),
                self.status_code
            )
            return self.render_to_json_response()
        if owner.verify_password(password):
            # todo: 重定向还是返回json信息
            login_admin(owner_wxid)
            self.json_data['success'] = u'管理员登录成功'
            logger.info(
                'POST -> admin login END: %s. Owner wxid: %s. Code: %s',
                owner_wxid,
                self.status_code
            )
            return self.render_to_json_response()
        else:
            self.json_data['errors']['authentication'] = u'密码错误'
            self.status_code = 403
            logger.info(
                u'POST -> admin login FAILED: 密码错误. Code: %s',
                self.status_code
            )
            return self.render_to_json_response()

gzone.add_url_rule(
    '/login',
    view_func=LoginView.as_view('login')
)


@gzone.route('/current', methods=['GET'])
def current():
    logger.debug('GET -> get current env params BEGIN.')
    _get_file_path = partial(
        gzone.media_processor.get_file_path,
        type_=gzone.media_processor.ACCOUNT_BANNER_IMG,
    )
    json_data = dict(
        is_admin=current_user.is_admin,
        banner_url=None if current_user.banner is None else url_for(
                    '.media',
                    filename=_get_file_path(current_user.banner.filename),
                    _external=True
                ),
        status_code=200
    )
    logger.debug(
        'GET -> get current env params END. Code: %s',
        200
    )
    return jsonify(json_data)


@gzone.route('/room_url/<room_id>')
def get_room_url(room_id):
    room_url = current_app.serializer.generate_room_url(room_id)


# for testing
@gzone.route('/clrsession', methods=['GET'])
def clear_session():
    next = request.args.get('next') or url_for('gzone.index')
    # 微信内置浏览器某些字段不能清除，fxxk!
    session.clear()
    return redirect(next)


@gzone.route('/logout_admin', endpoint='logout', methods=['POST'])
@admin_required
def log_admin_out():
    logger.debug('POST -> logout admin BEGIN.')
    logout_admin()
    logger.debug(
        'POST -> logout admin END. Code: %s',
        200
    )
    return jsonify({'success': u'注销管理员成功', 'code': 200})


# for testing
@gzone.route('/set_session', methods=['POST'])
def set_session():
    session.clear()
    openid = request.form.get('openid') or 'ovj3E0naN40vx4-_XIlvX0R5Ra7w'
    session['openid'] = openid

    return jsonify({'success': u'设置openid成功', 'code': 200})


# for testing
@gzone.route('/info', methods=['GET'])
def info():
    return render_template('info.html')


# put this snippet at the very end of this module
# 因为前端静态文件的请求采用相对路径的方式,所以需要对nginx进行同步修改
@gzone.route('/r/<enc_room_id>', methods=['GET'])
def index():
    logger.debug('GET -> enter into room')
    return render_template('gzone/gzone.html')
